/*
 * Copyright 2009-2010 the original author or authors.
 *
 * Licensed under the Apache license, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.internna.iwebjtracker.model;

import java.util.Set;
import java.util.Date;
import java.util.HashSet;
import java.util.Collection;
import java.util.Collections;
import javax.persistence.Table;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import javax.persistence.PrePersist;
import javax.persistence.NamedQuery;
import javax.persistence.CascadeType;
import javax.persistence.FetchType;
import javax.persistence.NamedQueries;
import org.hibernate.validator.NotEmpty;
import org.apache.commons.collections.Predicate;
import org.directwebremoting.annotations.RemoteProperty;
import org.directwebremoting.annotations.DataTransferObject;
import org.internna.iwebmvc.utils.Assert;
import org.internna.iwebmvc.metadata.EntitySecurity;
import org.internna.iwebmvc.metadata.EntitySecurityRule;
import org.internna.iwebmvc.metadata.EntitySecurity.CRUD;
import org.internna.iwebjtracker.project.validation.Errors;
import org.internna.iwebjtracker.activity.model.RecordedActivity.ActivityType;

import static org.internna.iwebmvc.utils.StringUtils.hasText;
import static org.internna.iwebmvc.utils.CollectionUtils.isNotEmpty;
import static org.apache.commons.collections.CollectionUtils.select;

/**
 * Project persistent entity and activity manager.
 *
 * @author Jose Noheda
 * @since 1.0
 */
@Entity
@Table(name = "PROJECTS")
@DataTransferObject(javascript = "DefaultProject")
@NamedQueries({
    @NamedQuery(name = "DefaultProject.findAll", query = "SELECT p FROM DefaultProject p"),
    @NamedQuery(name = "DefaultProject.findByAlias", query = "SELECT p FROM DefaultProject p WHERE alias = :alias")
})
@EntitySecurity({
    @EntitySecurityRule(operation = CRUD.SAVE_OR_UPDATE, ifAnyGranted = {"ROLE_PROJECT_MANAGER"})
})
public class DefaultProject extends ProjectMembershipManager<DefaultModule, DefaultVersion, DefaultProject> {

    private static final long serialVersionUID = 1876611327691946317L;

    /**
     * Named query to find a project by the alias.
     */
    public static final String FIND_BY_ALIAS = "DefaultProject.findByAlias";

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "parentProject")
    private Set<DefaultModule> modules;

    @NotEmpty
    @RemoteProperty
    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "parentProject")
    private Set<DefaultVersion> versions;

    /**
     * Accessor method. Delegates to getAllVersions() to comply with the interface.
     */
    public Set<DefaultVersion> getVersions() {
        return getAllVersions();
    }

    /**
     * Adds a new version as long as it has a valid label. If it's te only version
     * available it's automatically promoted to be the current version.
     *
     * @param version any
     */
    public final void addVersion(DefaultVersion version) {
        if ((version != null) && (hasText(version.getLabel()))) {
            if (versions == null) {
                versions = new HashSet<DefaultVersion>();
                version.setCurrentVersion(true);
            }
            onAddVersion(version);
            versions.add(version);
        }
    }

    /**
     * Sets this project as the parent one.
     *
     * @param version a non null instance
     */
    protected final void onAddVersion(DefaultVersion version) {
        version.setParentProject(this);
    }

    /**
     * Creates a version with the provided label as long as the text is readable.
     *
     * @param version any
     */
    @Override public final DefaultVersion createVersion(String version, Date date) {
        DefaultVersion defaultVersion = null;
        if (hasText(version)) {
            defaultVersion = new DefaultVersion();
            defaultVersion.setLabel(version);
            defaultVersion.setDueDate(date);
            addVersion(defaultVersion);
        }
        return defaultVersion;
    }

    /**
     * All versions in this project that have no parent version (top of hierarchy).
     *
     * @return a non modifiable group of versions
     */
    @Override public Set<DefaultVersion> getAllVersions() {
        if (isNotEmpty(versions)) {
            Set<DefaultVersion> filtered = new HashSet<DefaultVersion>(select(versions, new Predicate() {
                @Override public boolean evaluate(Object object) {
                    if (object instanceof DefaultVersion) {
                        return ((DefaultVersion) object).getParentVersion() == null;
                    }
                    return false;
                }
            }));
            return Collections.unmodifiableSet(filtered);
        }
        return Collections.EMPTY_SET;
    }

    /**
     * Tests for a provided version name in the collection returned by getAllVersions().
     *
     * @param name any
     * @return any
     */
    @Override public DefaultVersion getVersionByName(String name) {
        DefaultVersion found = null;
        for (DefaultVersion version : getAllVersions()) {
            String label = version.getLabel();
            if (hasText(label) && (label.equals(name))) {
                found = version;
            }
        }
        return found;
    }

    /**
     * Iterates all the versions looking for one with the current flag set to true.
     */
    @Override public final DefaultVersion getCurrentVersion() {
        DefaultVersion current = null;
        for (DefaultVersion version : getAllVersions()) {
            if (version.isCurrentVersion()) {
                current = version;
            }
        }
        return current;
    }

    /**
     * Clears the current flag attribute of the only version that has it and puts it
     * in the provided one as long as it is a valid version in this project
     *
     * @param version any
     * @return true if the provided version has been tagged as current
     */
    @Override public boolean setAsCurrentVersion(DefaultVersion version) {
        boolean changed = false;
        if ((version != null) && (version.getParentVersion() == null) && (this.equals(version.getParentProject()))) {
            getCurrentVersion().setCurrentVersion(false);
            version.setCurrentVersion(true);
            changed = true;
        }
        return changed;
    }

    /**
     * Adds a module filtering the null ones.
     *
     * @param module any
     */
    @Override public final synchronized DefaultModule addModule(DefaultModule module) {
        if (module != null) {
            if (modules == null) {
                modules = new HashSet<DefaultModule>();
            }
            modules.add(module);
            module.setParentModule(null);
            onAddModule(module);
        }
        return module;
    }

    /**
     * Sets this project as the parent one.
     *
     * @param module a non null instance
     */
    protected final void onAddModule(DefaultModule module) {
        module.setParentProject(this);
    }

    /**
     * Creates a new module from name and description. It sets parent/child relations in the process.
     *
     * @param moduleName a non empty string
     * @param moduleDescription a non empty string
     * @return a non null module
     */
    @Override public final DefaultModule createModule(String moduleName, String moduleDescription) {
        Assert.hasText(moduleName);
        Assert.hasText(moduleDescription);
        DefaultModule module = new DefaultModule();
        module.setModuleName(moduleName);
        module.setDescription(moduleDescription);
        addModule(module);
        onCreateModule(module);
        return module;
    }

    /**
     * Delegates to onAddModule(module)
     *
     * @param module a non null instance
     */
    protected final void onCreateModule(DefaultModule module) {
        onAddModule(module);
    }

    /**
     * All modules that have no parent module (no components).
     *
     * @return a not empty unmodifiable collection of modules
     */
    @Override public Collection<DefaultModule> getModules() {
        if (isNotEmpty(modules)) {
            Set<DefaultModule> filtered = new HashSet<DefaultModule>(select(modules, new Predicate() {
                @Override public boolean evaluate(Object object) {
                    if (object instanceof DefaultModule) {
                        return ((DefaultModule) object).getParentModule() == null;
                    }
                    return false;
                }
            }));
            return Collections.unmodifiableSet(filtered);
        }
        return Collections.EMPTY_SET;
    }

    /**
     * Fills the reverse side of the collection (trackings).
     *
     * @param user a non null instance
     */
    @Override protected final void onAddTracking(TrackerUser user) {
        user.addTracking(this);
    }

    /**
     * Fills the reverse side of the collection (memberships).
     *
     * @param user a non null instance
     */
    @Override protected final void onAddMembership(TrackerUser user) {
        user.addMembership(this);
        createActivity(ActivityType.PROJECT_NEW_MEMBER, user);
    }

    /**
     * Sets this project as the parent one in the activity provided.
     *
     * @param activity a non null instance
     */
    @Override protected final void onCreateActivity(Activity activity) {
        if (activity != null) {
            activity.setProject(this);
        }
    }

    /**
     * An a project is closed if it is not open.
     *
     * @return !isOpen()
     */
    @Override public final boolean isClosed() {
        return !isOpen();
    }

    /**
     * Closes this project and generates a new activity.
     *
     * @param user any
     * @return
     */
    @Override public final Errors close(TrackerUser user) {
        return openOrClose(user, true);
    }

    /**
     * Reopens the project and generates a new activity.
     *
     * @param user
     * @return
     */
    @Override public Errors reopen(TrackerUser user) {
        return openOrClose(user, false);
    }

    private Errors openOrClose(TrackerUser user, boolean close) {
        if ((user != null) && isAdministrator(user)) {
            setOpen(!close);
            createActivity(close ? ActivityType.PROJECT_CLOSE : ActivityType.PROJECT_OPEN, user);
            return null;
        }
        return Errors.PROJECT_NOT_AN_ADMINISTRATOR;
    }

    /**
     * Adds a new user to the observer list.
     *
     * @param user any (if null no operation performed)
     * @return any
     */
    @Override public boolean join(TrackerUser user) {
        if ((user != null) && isPublicView() && (!isDeveloper(user))) {
            addObserver(user);
            createActivity(ActivityType.JOIN_PROJECT, user);
            return true;
        }
        return false;
    }

    /**
     * Checks if the value has actually changed before clearing all observers if the project has become private.
     *
     * @param publicView any
     */
    @Override public final void setAsPublic(boolean publicView) {
        if (publicView != isPublicView()) {
            setPublicView(publicView);
            if (!publicView) {
                setObservers(null);
                for (TrackerUser developer : getDevelopers()) {
                    addObserver(developer);
                }
            }
        }
    }

    /**
     * Sets Maven group and artifact identifiers (always both of them).
     *
     * @param groupId any
     * @param artifactId any
     */
    @Override public void setMavenInfo(String groupId, String artifactId) {
        if (hasText(groupId) && hasText(artifactId)) {
            setMavenGroupId(groupId);
            setMavenArtifactId(artifactId);
        }
    }

    /**
     * Invoked by the persistence provider automatically to establish
     * the open status for a newly created project.
     */
    @PrePersist protected final void prePersist() {
        setOpen(true);
    }

    /**
     * Converts this project to a readable string usable in logs.
     *
     * @return name, version, modules of this project
     */
    @Override public final String toString() {
        return "project=[name: " + getProjectName() + ", version: " + getCurrentVersion() + ", modules: " + getModules() + "]";
    }

}
